home *** CD-ROM | disk | FTP | other *** search
- [
- This was written for a local newsletter. Any people mentioned
- are locals.
-
- Alan Kent
- 22 Wallabah St
- Mt Waverley 3149
-
- Alan Kent (RMIT, Melbourne, AUSTRALIA)
- UUCP: {seismo,hplabs,mcvax,ukc,nttlab}!munnari!goanna.oz!ajk
- ARPA: munnari!goanna.oz!ajk@SEISMO.ARPA
- ACSnet: ajk@goanna.oz
- ]
-
-
- AN INTRODUCTION TO WRITING
- YOUR OWN DEVICE DRIVERS
-
- Ron Wail and co. are not the only ones who have been
- working on a hard disk for the
- amiga. I have also been working on one for some time now
- in the little spare time I
- have. As I am currently doing my masters
- in computer science at RMIT, I
- do not have as much time as I would like to spend on the machine.
- If you wish to buy a hard disk you will have to buy one off Ron and
- friends as I have no intension of building and selling them.
- If you want to take on the challenge of building your own,
- then read on!
-
- Getting the additional hardware needed to add a hard disk
- to work was trivial compared
- to the problems I had trying to get the software to work (this
- may be because my brother John designed the hardware for me).
- I used a WD-1002-05 controller card as my brother had one lying
- around at home. I recently rang Daneva Australia in Sandringham
- and they said the boards now cost approximately $550 plus 20% tax
- (single quantities). Perhaps there are cheaper boards around these days.
- The controller card can in fact handle 3 hard disks and 4 five
- and a quarter inch floppy drives so you can also add some standard
- 5 inch floppies too if you like.
- Using a prebuilt controller card made the hardware much simpler -
- in fact, only 7 additional IC's were needed
- for decoding and timing. At present I have the IC's on a wire
- wrap board which plugs into the expansion slot on the side of the
- amiga. Someday I hope to add a bit more memory and a battery backup
- clock. I will explain the hardware in more detail later.
-
- In this article, I thought I would explain some of the
- fundamentals of trying to write a device driver. It may not be
- enough for you to write your own driver immediately,
- but its a step in the right direction.
- Much of the information here is relevant for
- any device driver, not just a hard disk device driver.
- All of the information in this
- article came from the manuals (somewhere), mainly the two
- volumes of the RKM (Rom Kernal Manual). Much of the hardware
- information I used came from the expansion specs which I ordered
- from Commordore in the states for US$40(?).
- As I am a C programmer by nature and dislike having to write
- assembly language code (although it is faster), I have
- added an extra twist to this project of trying to implement my drivers in C.
- I think I should point out at this stage that it is almost impossible
- to add hard disks to V1.1 (I tried for weeks, but I just could not
- get AmigaDOS to realise that the hard disk existed)
- It is very easy however with V1.2.
-
- THE EXEC AND AMIGADOS
-
- Before we can really get into device drivers, some basics of
- the multitasking executive (called the exec) must be known. The exec
- is the low level program that has a number of responsibilities, the
- main responsibility being to decide which task is to
- run next on the amiga. A task is simply an occurance of a program
- running. For example, if two CLI windows are open at the same time,
- then there are two CLI tasks running.
- Each task on the amiga is given its own memory space and stack space.
- Details about the task are kept in a task structure such as where
- its stack space is and the contents of the task's registers
- when the task is not running.
- The exec also provides mechanisisms for supporting communication
- between tasks, suppoirt for libraries of functions and support for devices
- as well as keeping track of what memory has been allocated.
-
- AmigaDOS is not part of the exec. It is another level of software
- which runs above the exec. AmigaDOS is basically a file management system
- which uses the exec functions. Its job is to keep
- track of where files reside on the disk and provide
- routines to read from and write to files.
- When AmigaDOS starts running a program, it actually starts a
- task, but adds some more information to the end of the task
- structure such where as to direct console input and output.
- Such tasks are called processes. Another slight difference is
- that a pointer to a process points to the first field in
- AmigaDOS's extra information after the task structure. The first entry
- happens to contain a pointer to a message port which AmigaDOS
- uses to communicate with the process (more on message ports later).
- Care must be taken to determine
- if a pointer is a pointer to a process or a task. In C, a process pointer
- can easily be converted to a task pointer using the following code.
-
- task = (struct Task *) ( ((char *)process) - sizeof(struct Task) )
-
- SIGNALS
-
- For one task to communicate to another task, the exec
- provides a number of functions. Each task keeps 32 signals.
- These signals can be set by other tasks using the Signal() function.
- The task receiving the signal can then use the Wait() function
- to wait for a signal to arrive.
- When the Wait() function is called, the exec will remove
- the task from the list of running tasks
- until the desired signal arrives. This means that
- no CPU time is used by the waiting process allowing more CPU time
- for other processes.
- A task can actually wait for any one of many different signals
- to arrive from many different tasks at the same time.
- Signals thus provide a very simple
- synchronisation method between tasks. One problem with
- signals however is that if the same signal is sent twice
- to a task before the
- receiving task gets a chance to have a look, it appears
- as if only one signal is received.
-
- MESSAGES
-
- Signals by themselves are not particularly useful. What is more
- useful is to be able to send some information from
- one task to another. Messages and Ports provide this
- ability. A message is simply a block of memory which has
- a special structure at the beginning of the block followed
- by any other information that is to be sent.
- In C, this can be achieved by defining a structure as follows:
-
- struct my_message {
- /* first the standard message information */
- struct Message msg;
- /* and now my details */
- int number;
- char character;
- int other_information;
- char *string;
- };
-
- The message structure definition can be found in the include
- file <exec/ports.h>. The Message structure has to be set up
- in a special way which is described in the RKM exec manual,
- page 34. Signals are used to notify a task that a new message
- has been received.
-
- PORTS
-
- Ok, now we have a message structure, but how do we get a
- message from one task to another? And how does one task know
- which of the 32 signals to send to the other task to notify it
- that a message has been sent? This is where ports become useful.
- A port is like putting a letter box outside your house. It provides
- a well known place that messages can be received.
- A port can be created using
- the function CreatePort() and when created, can be
- assigned a unique name. The function FindPort() can be used
- to search the whole system for a port by its name. When a port is created,
- it is also allocated a signal number which is used to tell
- the receiving task that a message has arrived.
-
- SENDING MESSAGES
-
- To send a message from one task to another then involves using the
- PutMsg() function. PutMsg() requires a pointer to the
- port (as returned by FindPort) and a pointer to the message
- to be sent. The actual sending of the message involves
- letting the receiving message port know where the message is in memory
- and then sending the receiving task a signal to let it know
- that a message has arrived. In order to allow many messages
- to be sent to a single port, messages are kept in a queue.
- This is why some special information is needed at the
- beginning of each message structure - list pointers must be
- maintained.
-
- The receiving task normally waits for messages to arrive
- using the WaitPort() function. This basically involves doing
- a Wait() for the signal bit assigned to that port. Once a
- message is received (or if a message has already been
- received and the program had not noticed it yet) WaitPort()
- will exit. Messages can then be removed form the port using
- the GetMsg() function. Note that due to the problem
- mentioned before about signals and the possibility of
- multiple signals appearing to be only a single signal, many
- calls to GetMsg() may be required. It is also possible that
- WaitPort() will return that a new message has arrived but it
- had already been processed - its just that the signal bit
- had not yet been cleared. The normal code structure for
- receiving messages can be safely performed using the
- following C code:
-
- struct Message *msg;
-
- while ( 1 ) { /* loop forever */
- WaitPort ( message_port );
- while ( ( msg = GetMsg ( message_port ) ) != NULL ) {
-
- /* do something with the message! */
-
- }
- }
-
- Until a message has been received and processed, the sending
- task should not modify the contents of the message. The
- PutMsg() call is basically granting the receiving task
- permission to use that piece of memory.
- Once the receiving task has finished
- with the message however, it may be useful to let the sending task
- know. This is in fact very important if information is to be
- returned to the sending task in the message structure. What
- is done then is for the sending task to also create a
- message port. This message port however is not given a name
- and so cannot be found using FindPort(). Instead, a pointer
- to this port is kept in the actual message structure
- (in the field mn_ReplyPort). The task that receives the message
- then uses the ReplyMsg() function to effectively send the
- message back to the original task. This means the the
- sending task must do a WaitPort() on the reply port so that
- it will wait until the message is sent back. This allows complete
- synchronization between the two tasks. Note that if the
- reply port is not put into the message structure, then
- ReplyMsg() does nothing.
-
- To close off a port so that no more messages can be
- received, use DeletePort(). The functions CreatePort() and
- DeletePort() are actually support functions written in C which
- should be in the libraries that come with your C compiler. These
- functions allocate/deallocate the necessary memory and
- call (if necessary) the functions AddPort() and RemPort().
- For more details on ports and some
- example code using them, see chapter 3 in RKM: exec.
-
- WHAT IS A DEVICE DRIVER?
-
- Well, now lets get into what this article is really about!
- A device driver is a piece of code which provides an
- interface between the amiga operating system or user
- programs and (usually) hardware on the system. There is one
- device driver for controlling the floppy disks, another for
- controlling the printer and so on. Drivers usually reside on
- disk until such time as they are opened. When a device
- is opened, the amiga searches through its list of known
- devices in memory and if it is not found, it then searches
- the devs directory on disk. This is why the first time you
- use the printer, the disk will become active for a period of
- time (to load the device driver) but when using the printer
- later, there may not be any extra disk activity (as the
- driver is already loaded). Note that the amiga can try to
- reclaim memory from a device driver if it is not in use and
- the amiga is low on free memory. Using C it is not easy to
- get this auto-load feature to work. As a result, I decided
- to ignore it by instead adding a RUN command in my
- startup-sequence file so that the driver was permanently
- loaded and could not be removed. This may not be very elligent,
- but it works!
-
- In order
- for a program to make a request to a device driver (for
- example the printer driver), an OpenDevice() call must be
- made. This
- call has 4 parameters: the name of the device you wish to
- open, the unit number you want (some devices such as the
- floppy disks can have several units - for efficency they all share
- the same device driver code), an IO request block
- and some flags which are interpreted by the
- device driver. The IO request block is a message with some extra
- information added (see struct IORequest in <exec/io.h>).
- This extra information includes a pointer to the device, a
- pointer to special memory allocated per unit, a command
- field, an error field and a flags field. As many devices
- perform much the same sort of commands (for example, writing
- to disk is not that much different than writing to a
- printer) a standard form of requests has been defined. The
- structure and basic commands defined are also in
- <exec/io.h>.
-
- Once a device is open, requests can be made using the IO
- request block passed to the OpenDevice() call using the
- functions CheckIO(), DoIO(), SendIO() and WaitIO().
- DoIO() performs an IO operation by sending the device driver
- the IO request as a message and waiting for the driver to
- complete (signaled by a ReplyMsg()). To be more precise, DoIO()
- tries to call the BeginIO entry point in the device driver directly
- (explained later) which will either
- immediately process the command and return, or else send
- the message to the port for processing when the driver is ready
- for it. SendIO() sends the request to the device driver's port,
- but does not wait for completion. This allows the
- task to continue doing other things. WaitIO() can then be
- used to wait for the command to complete. CheckIO()
- provides a means of testing if the operation is complete
- without waiting for it if it has not. When CheckIO() says
- the operation is complete, WaitIO() must still be called to
- clear the signal bit. The RKM: libraries and devices
- contains many pieces of example code opening devices,
- sending commands and closing devices. Note that it is very
- important to close a device when finished with it as some of
- the devices only allow one task to open them at a time.
-
- WRITING A DEVICE DRIVER
-
- So now that we know what a device driver is, how do we go
- about writing one? In the RKM: libraries and devices
- appendix F a sample device driver written in assembler is
- shown. Looking at the code, I suspect it contains a few errors.
- Some pieces of code just didn't make sense to me. After
- looking at it for a while and (hopefully) understanding how
- it works, I threw it away and started again from scratch.
- The example in the manual is meant to be able to
- automatically load from disk. As I mentioned before, I
- did not worry about this as I wanted to get something
- working as quickly as possible and automatically loading C
- programs looked like it could be a bit of a problem
- (actually, one of the manuals stated it was not possible).
-
- When writing a device driver, there are two main sections of code.
- The first section looks very much like a library - and in
- fact shares many structures and functions with libraries.
- This provides a nice consistant interface between the device
- driver and other programs that wish to use it.
- The second section is what actually performs all the
- commands.
-
- First, a set of routines must be written which allow a
- device to be opened, closed and expunged (totally removed from
- memory). These are exactly the same as for a library. On top
- of these functions, two extra functions must be defined - BeginIO
- and AbortIO. BeginIO is a call which tries to
- immediately perform the IO operation without using the
- message port. If the operation cannot be done immediately,
- then the command must be queued by sending it to the message
- port. This allows simple status commands to be performed very
- quickly. The AbortIO command tries to abort an existing
- command. One problem with writing a driver in C code is that
- the exec passes parameters for these calls in registers.
- This means that some pieces of assembler must be written
- to push the registers onto the stack before calling the C code.
- Note that as the operating system is calling functions written
- in C directly, if the C compiler requires special values in
- the registers then the code will not work. For example, I have
- heard that the Aztec C compiler uses a register to point to the
- global variables. If this is the case, then this register must
- be set up (probably by the same code that pushes the registers
- onto the stack) before the actual C function can be called.
-
- Once these routines are written a device structure
- (struct Device) must be created using the MakeLibrary()
- call. MakeLibrary() accepts a pointer to an area of memory
- that defines how the library node is to be initialized (see
- the function InitStruct()). However it is very difficult to
- set up the necessary memory area for InitStruct() in C and so I simply
- initialize the device structure after calling MakeLibrary()
- using normal C code.
- Once this has been done, AddDevice() must be called to
- add the new device to the system. Once AddDevice() has been
- called, other programs may use all of the device function
- calls previously mentioned (OpenDevice() etc).
-
- The example device driver in the manual
- creates a new process for each unit. Rather than trying to
- make more problems for myself by trying to create new
- processes from a C program, and as I only have a single unit
- anyway, I did not create a new process per unit but rather
- used the same process as creates and adds the device node
- to receive and process commands.
- So, before the device is actually added to the system, a
- port must be created to recieve commands. The Open code will
- put a pointer to this port into the IO request block which
- was passed in the OpenDevice() call.
- This is how DoIO() and SendIO() know where to send the message.
-
- THE HARD DISK DEVICE DRIVER
-
- The above discussion should be able to be used for any type of device
- driver you wish to write. I have been considering trying to
- write a printer spooler using device drivers too, although
- surely someone has done a printer spooler for the amiga
- before (I had better go search through all the PD disks I guess).
-
- The hard disk driver must emulate the trackdisk device driver in
- every way. Legal requests are defined in chapter 7 of
- the RKM: libraries and devices and include such commands as
- read data from disk, write data to disk, format a track,
- switch the motor on and so on. The commands are
- all fairly straight forward. The
- hard disk driver is much simpler than the floppy disk driver
- in many ways as the disk cannot
- be removed!
-
- One area I found difficult to process was the
- sector labels. The trackdisk device actually allows an extra
- 16 bytes per sector to be written due to some sector header
- information on the disk. The hard disk controller I have
- does not allow this. As a result, I mark any commands that
- try to use this information as an error. So far, I have not
- found any code that actually uses this information so it
- is not a problem.
-
- The trackdisk device driver (in V1.1 anyway)
- buffers a whole track of the disk in memory at a time.
- This buffering of data in memory is referred to as caching.
- V1.2 does better caching, but I dont know how (I dont have
- all the necessary documentation for V1.2).
- As a result, I tried to add some of my own more intelligent
- caching. Unfortunately, my caching seems to work quite well
- for about 5 minutes, but then it hangs the system. I will
- have to recheck the code sometime. It is difficult to
- determine however if the caching actually improves the speed of the
- hard disk or whether it runs slower due to the extra code
- that needs to be executed. The extra caching certainly uses
- up more memory, so at this stage I have removed all caching.
- Even without caching, its still faster than floppies.
-
- My hard disk driver then consists of a number of sections.
- First, when it is loaded it creates and adds the device node
- to the system. It then waits for comands to arrive from a
- message port. When a command arrives, a switch statement
- decides what code to execute based on the command type.
- The disk read and write type commands map the offset and
- length fields in the command message onto head, track and
- sector numbers which is then fed to the hard disk controller.
- Other commands such as motor on/off and disk change count
- can be easily immitated (the motor can never be switched off
- and the disk change count is constant).
-
- One area that can be confusing is that
- the code that is needed for handling open and close device
- calls made by programs making requests to the driver is
- never executed by the device driver process. When the device
- is added to the system, a set of pointers to these functions
- is put in the device structure allowing other tasks to execute
- code.
-
- To install the driver involves adding a few lines to your
- S:STARTUP-SEQUENCE file. First the command RUN L:HARDDISK
- (where L:HARDDISK is my device driver program) starts up
- the device driver. Next the command MOUNT DH0: notifies
- AmigaDOS that the device can be used as a disk. For the
- mount command to work, the file DEVS:MOUNTLIST must have
- an entry for DH0: added. The file on my disk had an example entry
- for DF1: so I just copied it and changed the fields (such
- as number of cylinders, heads etc) for my hard disk.
- The name of the device driver also had to be changed from
- trackdisk.device to harddisk.device. I have also added
- to my S:STARTUP-SEQUENCE file
- some ASSIGN commands to change the C: directory to be on
- the hard disk. These changes need
- only be made once and then every time you boot up, you
- have a very big disk drive!
- One problem I did have however was to make sure that the
- hard disk driver was actually loaded before I started using
- it. This is because the RUN command exits before the program
- to be run has even been loaded from disk.
- After the RUN command I put in a delay of about 5 seconds
- in the S:STARTUP-SEQUENCE file
- so that the driver should have time to load completely.
-
- HARDWARE
-
- This is a brief description of the hardware I have used.
- It does not auto-configure (oh dear, Commodoore will never
- support me now) as I could not work out exactly how to
- do it. I have a copy of the expansion specs from Commodoore
- (I sent off to the states) but the auto-configure was a bit
- beyond the effort I wanted to go to. I am not likely to
- add anything else to my amiga anyway (famous last words).
- The controller card I used has 8 registers which I have mapped into
- memory space. Due to
- the 68000's 16 bit words, it ended up using 16 bytes of memory,
- the high bytes of the 16 bit words not being used.
- The controller's registers can be
- read from or written to and include cyclinder number registers,
- status registers, command registers and so on. Sending commands
- to the hard disk simply involes storing the relevant parameters
- in the controllers registers and sending a command such as
- read a sector, write a sector or format a track.
- All this is done by simple memory read and write commands.
- I ended up putting the controller card at the top of expansion
- memory at $9ffff0 (actually I do not decode the address to that
- precision, but its close). As long as I dont expand to 8 megs
- of memory, it should not interfere with anything - even other
- boards which do autoconfigure.
-
- After sending the controller a command, the device driver must
- wait for the command to finish. I tried to use the interrupt line
- provided by the controller card, but at this stage I have not
- successfully got the interrupt handlers on the amiga to work. The approach
- I am currently using is to poll the status register of the controller.
- Polling is where the program sits in a loop continuously checking
- the status register until the command has finished. In order to
- give other tasks a chance to run while the driver is polling, I
- placed a call to Delay() inside the polling loop. The delay cannot
- be too long or else the disk will become too slow, but it does give
- other tasks a bit of a chance to run.
-
- Looking at the side of the amiga, the expansion bus is numbered
- as follows. Reading the hardware reference manual seems to say
- the numbering is different to this, but this is what I used (and
- it works). I would check the signal numbers before building this
- circuit in case of incorrect pin numbers due to typing errors.
-
-
- 1 3 5 7 9 ...... 85
- =================== the edge of the board
- 2 4 6 8 10 ..... 86
-
-
- WARNING: I do not take any responsibility if the following circuit
- contains any errors. All I can say is that it has not blown my
- amiga up yet. USE AT OWN RISK!
-
- All numbers on the left are amiga signals. All numbers on the right
- are WD-1002-05 controller signals. Parts should be LS or better for speed.
- All signals marked * are active when low (inverted). Wires that cross like
-
- |
- ---------
- |
-
- are not joined. Joins are shown by +'s as in
-
- |
- ----+----
- |
-
-
- 7404
- 1|\ 2 1|
- 74 AS* -----| >o----|
- |/ 2|
- 59 A23 -------------|
- 3|\ 4 3|
- 57 A22 -----| >o----|---\
- |/ | |8
- 5|\ 6 4|7430|o-----+
- 58 A21 -----| >o----| | |
- |/ 5|---/ |
- 56 A20 -------------| |
- 6| |
- 54 A19 -------------| |
- 11| |
- 52 A18 -------------| |
- 12| |
- 47 A17 -------------| |
- | | Address Decoding
- |
- 1| |
- 45 A16 -------------| |
- 2| |
- 43 A15 -------------| |
- 3| |
- 41 A14 -------------|---\ |
- 4| | |
- 39 A13 -------------|7430|o--+ |
- 5| | | |
- 38 A12 -------------|---/ | |
- 6| | |
- 36 A11 -------------| | | ^
- 11| | | |
- 34 A10 -------------| 4o 5o 6|
- 12| +------------+
- 32 A9 -------------| | E0*E1*E2 O7|o-7--$9FFFC0--+-------- CS* 23
- | | O6|o-9--$9FFF80 |
- 3| O5|o-10-$9FFF40 |
- 30 A8 ------------------|A2 O4|o-11-$9FFF00 |
- 2| 74138 O3|o-12-$9FFEC0 |9
- 28 A7 ------------------|A1 O2|o-13-$9FFE80\---/
- 1| O1|o-14-$9FFE40 \ /7404
- 23 A6 ------------------|A0 O0|o-15-$9FFE00 o8
- +------------+ |
- |
- ^ ^ |
- | | |
- ^ 4o 10o |
- | +-------+ +-------+ |
- | 2| P |5 12| P |9 |
- +---|D Q|-------------|D Q|-- |
- | 7474 | | 7474 | | Timing
- 16 C1 ------+---|> Q*|o--- +----|> Q*|o--------+
- | 3| C |6 | 11| C |8 | |
- | +-------+ | +-------+ | |
- | 1o | 13o | |
- | | | | | | 1
- +--------------------+ | | +--|----\ 3
- | | | |7438 |o--+
- +---------------------+----------+-----|----/ |
- 2 (openC) |
- |
- 18 XRDY* ------------------------------------------------------------+
-
- 13|\ 12 1
- 68 R/W* ------+------| >o-------|----\ 3
- | |/ 2|7400 |o--------- WR* 25
- | +--|----/
- | 11|\ 10 |
- 70 LDS* -------------| >o----+--|----\ 6
- | |/ 4|7400 |o--------- RD* 27
- +-----------------|----/
- 5
- 4
- 6 /---|-----+
- 19 INT2* --------o|7438| +------------------- INTRQ 35
- \---|-----+
- (openC) 5 Connect this only if want interrupts
- after every command is finished
-
- 21 A5 ------- (not used)
- 24 A4 ------- (not used)
- 26 A3 ------- RS2 21
- 27 A2 ------- RS1 19
- 29 A1 ------- RS0 17
- 53 RES* ----- MR* 39
- 75 PD0 ------ DAL0 1
- 77 PD1 ------ DAL1 3
- 79 PD2 ------ DAL2 5
- 81 PD3 ------ DAL3 7
- 83 PD4 ------ DAL4 9
- 86 PD5 ------ DAL5 11
- 84 PD6 ------ DAL6 13
- 82 PD7 ------ DAL7 15
-
-
-
- by Alan Kent
-